package org.go.trigger;

import java.text.ParseException;
import java.util.Date;
import java.util.TimeZone;

import org.go.domain.GoDomain;
import org.go.expcetion.JobExecutionException;
import org.go.scheduler.ScheduleBuilder;
import org.go.scheduler.Scheduler;
import org.go.work.WorkDataMap;
import org.go.work.WorkKey;

/**
 * 
 * @author hejie
 *
 */
public class CronTrigger implements Trigger {
	/**
	 * Required for serialization support. Introduced in Quartz 1.6.1 to 
	 * maintain compatibility after the introduction of hasAdditionalProperties
	 * method. 
	 * 
	 * @see java.io.Serializable
	 */
	private static final long serialVersionUID = -8644953146451592766L;

	protected static final int YEAR_TO_GIVEUP_SCHEDULING_AT = CronExpression.MAX_YEAR;

	private String calendarName = null;

	private CronExpression cronEx = null;

	private String description = null;

	private Date endTime = null;

	private String id;

	private TriggerKey key = null;

	private Date nextFireTime = null;

	private Date previousFireTime = null;

	private Date startTime = null;

	private transient TimeZone timeZone = null;

	/**
	 * <p>
	 * Create a <code>CronTrigger</code> with no settings.
	 * </p>
	 * 
	 * <p>
	 * The start-time will also be set to the current time, and the time zone
	 * will be set the the system's default time zone.
	 * </p>
	 */
	public CronTrigger() {
		super();
		setStartTime(new Date());
		setTimeZone(TimeZone.getDefault());
	}

	protected Date getTimeAfter(Date afterTime) {
		return (cronEx == null) ? null : cronEx.getTimeAfter(afterTime);
	}

	/**
	 * NOT YET IMPLEMENTED: Returns the time before the given time
	 * that this <code>CronTrigger</code> will fire.
	 */
	protected Date getTimeBefore(Date endTime) {
		return (cronEx == null) ? null : cronEx.getTimeBefore(endTime);
	}

	@Override
	public Trigger clone() {
		Trigger copy;
		try {
			copy = (Trigger) super.clone();
			// Shallow copy the jobDataMap.  Note that this means that if a user
			// modifies a value object in this map from the cloned Trigger
			// they will also be modifying this Trigger. 
			//			if (jobDataMap != null) {
			//				copy.jobDataMap = (JobDataMap) jobDataMap.clone();
			//			}
		} catch (CloneNotSupportedException ex) {
			throw new IncompatibleClassChangeError("Not Cloneable.");
		}
		return copy;
	}

	@Override
	public int compareTo(Trigger other) {
		if (other.getKey() == null && getKey() == null)
			return 0;
		if (other.getKey() == null)
			return -1;
		if (getKey() == null)
			return 1;

		return getKey().compareTo(other.getKey());
	}

	/**
	 * <p>
	 * Called by the scheduler at the time a <code>Trigger</code> is first
	 * added to the scheduler, in order to have the <code>Trigger</code>
	 * compute its first fire time, based on any associated calendar.
	 * </p>
	 * 
	 * <p>
	 * After this method has been called, <code>getNextFireTime()</code>
	 * should return a valid answer.
	 * </p>
	 * 
	 * @return the first time at which the <code>Trigger</code> will be fired
	 *         by the scheduler, which is also the same value <code>getNextFireTime()</code>
	 *         will return (until after the first firing of the <code>Trigger</code>).
	 *         </p>
	 */
	@Override
	public Date computeFirstFireTime(Calendar calendar) {
		nextFireTime = getFireTimeAfter(new Date(getStartTime().getTime() - 1000l));

		while (nextFireTime != null && calendar != null && !calendar.isTimeIncluded(nextFireTime.getTime())) {
			nextFireTime = getFireTimeAfter(nextFireTime);
		}

		return nextFireTime;
	}

	@Override
	public String getCalendarName() {
		return calendarName;
	}

	/* (non-Javadoc)
	 * @see org.quartz.CronTriggerI#getCronExpression()
	 */
	@Override
	public String getCronExpression() {
		return cronEx == null ? null : cronEx.getCronExpression();
	}

	@Override
	public String getDescription() {
		return null;
	}

	/**
	 * <p>
	 * Get the time at which the <code>CronTrigger</code> should quit
	 * repeating - even if repeastCount isn't yet satisfied.
	 * </p>
	 * 
	 * @see #getFinalFireTime()
	 */
	@Override
	public Date getEndTime() {
		return this.endTime;
	}

	/* (non-Javadoc)
	 * @see org.quartz.CronTriggerI#getExpressionSummary()
	 */
	@Override
	public String getExpressionSummary() {
		return cronEx == null ? null : cronEx.getExpressionSummary();
	}

	/**
	 * <p>
	 * NOT YET IMPLEMENTED: Returns the final time at which the 
	 * <code>CronTrigger</code> will fire.
	 * </p>
	 * 
	 * <p>
	 * Note that the return time *may* be in the past. and the date returned is
	 * not validated against org.quartz.calendar
	 * </p>
	 */
	@Override
	public Date getFinalFireTime() {
		Date resultTime;
		if (getEndTime() != null) {
			resultTime = getTimeBefore(new Date(getEndTime().getTime() + 1000l));
		} else {
			resultTime = (cronEx == null) ? null : cronEx.getFinalFireTime();
		}

		if ((resultTime != null) && (getStartTime() != null) && (resultTime.before(getStartTime()))) {
			return null;
		}

		return resultTime;
	}

	@Override
	public String getFireInstanceId() {
		return null;
	}

	/**
	 * <p>
	 * Returns the next time at which the <code>CronTrigger</code> will fire,
	 * after the given time. If the trigger will not fire after the given time,
	 * <code>null</code> will be returned.
	 * </p>
	 * 
	 * <p>
	 * Note that the date returned is NOT validated against the related
	 * org.quartz.Calendar (if any)
	 * </p>
	 */
	@Override
	public Date getFireTimeAfter(Date afterTime) {
		if (afterTime == null) {
			afterTime = new Date();
		}

		if (getStartTime().after(afterTime)) {
			afterTime = new Date(getStartTime().getTime() - 1000l);
		}

		if (getEndTime() != null && (afterTime.compareTo(getEndTime()) >= 0)) {
			return null;
		}

		Date pot = getTimeAfter(afterTime);
		if (getEndTime() != null && pot != null && pot.after(getEndTime())) {
			return null;
		}

		return pot;
	}

	@Override
	public long getInterval() {
		return 0;
	}

	@Override
	public WorkDataMap getJobDataMap() {

		return null;
	}

	@Override
	public WorkKey getJobKey() {
		return null;
	}

	@Override
	public TriggerKey getKey() {
		return key;
	}

	@Override
	public int getMisfireInstruction() {
		return 0;
	}

	/**
	 * <p>
	 * Returns the next time at which the <code>Trigger</code> is scheduled to fire. If
	 * the trigger will not fire again, <code>null</code> will be returned.  Note that
	 * the time returned can possibly be in the past, if the time that was computed
	 * for the trigger to next fire has already arrived, but the scheduler has not yet
	 * been able to fire the trigger (which would likely be due to lack of resources
	 * e.g. threads).
	 * </p>
	 *
	 * <p>The value returned is not guaranteed to be valid until after the <code>Trigger</code>
	 * has been added to the scheduler.
	 * </p>
	 *
	 * @see TriggerUtils#computeFireTimesBetween(Trigger, org.quartz.Calendar , Date, Date)
	 */
	@Override
	public Date getNextFireTime() {
		return this.nextFireTime;
	}

	/**
	 * <p>
	 * Returns the previous time at which the <code>CronTrigger</code> 
	 * fired. If the trigger has not yet fired, <code>null</code> will be
	 * returned.
	 */
	@Override
	public Date getPreviousFireTime() {
		return this.previousFireTime;
	}

	@Override
	public int getPriority() {
		return 0;
	}

	@Override
	public int getRepeatCount() {

		return 0;
	}

	@Override
	public char[] getRepeatInterval() {

		return null;
	}

	/**
	 * Get a {@link ScheduleBuilder} that is configured to produce a 
	 * schedule identical to this trigger's schedule.
	 * 
	 * @see #getTriggerBuilder()
	 */
	@Override
	public ScheduleBuilder<Trigger> getScheduleBuilder() {
		CronScheduleBuilder cb = null;
		try {
			cb = CronScheduleBuilder.cronSchedule(getCronExpression()).inTimeZone(getTimeZone());
		} catch (ParseException ignore) {
			// can't happen (because the expression was validated to get here in the first place)
		}

		switch (getMisfireInstruction()) {
		case GoDomain.MISFIRE_INSTRUCTION_DO_NOTHING:
			cb.withMisfireHandlingInstructionDoNothing();
			break;
		case GoDomain.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:
			cb.withMisfireHandlingInstructionFireAndProceed();
			break;
		}
		return cb;
	}

	/**
	 * <p>
	 * Get the time at which the <code>CronTrigger</code> should occur.
	 * </p>
	 */
	@Override
	public Date getStartTime() {
		return this.startTime;
	}

	@Override
	public int getTimesTriggered() {
		return 0;
	}

	@Override
	public TimeZone getTimeZone() {

		if (cronEx != null) {
			return cronEx.getTimeZone();
		}

		if (timeZone == null) {
			timeZone = TimeZone.getDefault();
		}
		return timeZone;
	}

	@Override
	public TriggerBuilder<? extends Trigger> getTriggerBuilder() {
		return null;
	}

	@Override
	public String getTriggerKey() {
		return id;
	}

	@Override
	public String getType() {
		return null;
	}

	/**
	 * Used by extensions of CronTrigger to imply that there are additional 
	 * properties, specifically so that extensions can choose whether to be 
	 * stored as a serialized blob, or as a flattened CronTrigger table. 
	 */
	@Override
	public boolean hasAdditionalProperties() {
		return false;
	}

	/**
	 * <p>
	 * Determines whether or not the <code>CronTrigger</code> will occur
	 * again.
	 * </p>
	 */
	@Override
	public boolean mayFireAgain() {
		return (getNextFireTime() != null);
	}

	@Override
	public void setCalendarName(String calendarName) {
		this.calendarName = calendarName;
	}

	/**
	 * Set the CronExpression to the given one.  The TimeZone on the passed-in
	 * CronExpression over-rides any that was already set on the Trigger.
	 * 
	 * @param cronExpression
	 */
	public void setCronExpression(CronExpression cronExpression) {
		this.cronEx = cronExpression;
		this.timeZone = cronExpression.getTimeZone();
	}

	public void setCronExpression(String cronExpression) throws ParseException {
		TimeZone origTz = getTimeZone();
		this.cronEx = new CronExpression(cronExpression);
		this.cronEx.setTimeZone(origTz);
	}

	@Override
	public void setDescription(String description) {
		this.description = description;
	}

	@Override
	public void setEndTime(Date endTime) {
		Date sTime = getStartTime();
		if (sTime != null && endTime != null && sTime.after(endTime)) {
			throw new IllegalArgumentException("End time cannot be before start time");
		}
		this.endTime = endTime;
	}

	@Override
	public void setFireInstanceId(String firedTriggerRecordId) {

	}

	@Override
	public void setInterval(long interval) {
		// TODO Auto-generated method stub

	}

	@Override
	public void setMisfireInstruction(int misFireInstr) {

	}

	/**
	 * <p>
	 * Sets the next time at which the <code>CronTrigger</code> will fire.
	 * <b>This method should not be invoked by client code.</b>
	 * </p>
	 */
	@Override
	public void setNextFireTime(Date nextFireTime) {
		this.nextFireTime = nextFireTime;
	}

	/**
	 * <p>
	 * Set the previous time at which the <code>CronTrigger</code> fired.
	 * </p>
	 * 
	 * <p>
	 * <b>This method should not be invoked by client code.</b>
	 * </p>
	 */
	@Override
	public void setPreviousFireTime(Date previousFireTime) {
		this.previousFireTime = previousFireTime;
	}

	@Override
	public void setPriority(int priority) {

	}

	@Override
	public void setStartTime(Date startTime) {
		if (startTime == null) {
			throw new IllegalArgumentException("Start time cannot be null");
		}

		Date eTime = getEndTime();
		if (eTime != null && startTime != null && eTime.before(startTime)) {
			throw new IllegalArgumentException("End time cannot be before start time");
		}

		// round off millisecond...
		// Note timeZone is not needed here as parameter for
		// Calendar.getInstance(),
		// since time zone is implicit when using a Date in the setTime method.
		java.util.Calendar cl = java.util.Calendar.getInstance();
		cl.setTime(startTime);
		cl.set(java.util.Calendar.MILLISECOND, 0);

		this.startTime = cl.getTime();
	}

	/**
	 * <p>
	 * Sets the time zone for which the <code>cronExpression</code> of this
	 * <code>CronTrigger</code> will be resolved.
	 * </p>
	 * 
	 * <p>If {@link #setCronExpression(CronExpression)} is called after this
	 * method, the TimeZon setting on the CronExpression will "win".  However
	 * if {@link #setCronExpression(String)} is called after this method, the
	 * time zone applied by this method will remain in effect, since the 
	 * String cron expression does not carry a time zone!
	 */
	public void setTimeZone(TimeZone timeZone) {
		if (cronEx != null) {
			cronEx.setTimeZone(timeZone);
		}
		this.timeZone = timeZone;
	}

	@Override
	public void setTriggerKey(TriggerKey key) {

	}

	/**
	 * <p>
	 * Called when the <code>{@link Scheduler}</code> has decided to 'fire'
	 * the trigger (execute the associated <code>Work</code>), in order to
	 * give the <code>Trigger</code> a chance to update itself for its next
	 * triggering (if any).
	 * </p>
	 * 
	 * @see #executionComplete(JobExecutionContext, JobExecutionException)
	 */
	@Override
	public void triggered(Calendar calendar) {
		previousFireTime = nextFireTime;
		nextFireTime = getFireTimeAfter(nextFireTime);
		while (nextFireTime != null && calendar != null && !calendar.isTimeIncluded(nextFireTime.getTime())) {
			nextFireTime = getFireTimeAfter(nextFireTime);
		}
	}

	/**
	 * <p>
	 * Updates the <code>CronTrigger</code>'s state based on the
	 * MISFIRE_INSTRUCTION_XXX that was selected when the <code>CronTrigger</code>
	 * was created.
	 * </p>
	 * 
	 * <p>
	 * If the misfire instruction is set to MISFIRE_INSTRUCTION_SMART_POLICY,
	 * then the following scheme will be used: <br>
	 * <ul>
	 * <li>The instruction will be interpreted as <code>MISFIRE_INSTRUCTION_FIRE_ONCE_NOW</code>
	 * </ul>
	 * </p>
	 */
	@Override
	public void updateAfterMisfire(Calendar cal) {
		int instr = getMisfireInstruction();

		if (instr == Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY)
			return;

		if (instr == MISFIRE_INSTRUCTION_SMART_POLICY) {
			instr = GoDomain.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW;
		}

		if (instr == GoDomain.MISFIRE_INSTRUCTION_DO_NOTHING) {
			Date newFireTime = getFireTimeAfter(new Date());
			while (newFireTime != null && cal != null && !cal.isTimeIncluded(newFireTime.getTime())) {
				newFireTime = getFireTimeAfter(newFireTime);
			}
			setNextFireTime(newFireTime);
		} else if (instr == GoDomain.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW) {
			setNextFireTime(new Date());
		}
	}

	/**
	 *  
	 * @see org.quartz.Trigger#updateWithNewCalendar(org.quartz.Calendar, long)
	 */
	@Override
	public void updateWithNewCalendar(Calendar calendar, long misfireThreshold) {
		nextFireTime = getFireTimeAfter(previousFireTime);

		if (nextFireTime == null || calendar == null) {
			return;
		}

		Date now = new Date();
		while (nextFireTime != null && !calendar.isTimeIncluded(nextFireTime.getTime())) {

			nextFireTime = getFireTimeAfter(nextFireTime);

			if (nextFireTime == null)
				break;

			//avoid infinite loop
			// Use gregorian only because the constant is based on Gregorian
			java.util.Calendar c = new java.util.GregorianCalendar();
			c.setTime(nextFireTime);
			if (c.get(java.util.Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
				nextFireTime = null;
			}

			if (nextFireTime != null && nextFireTime.before(now)) {
				long diff = now.getTime() - nextFireTime.getTime();
				if (diff >= misfireThreshold) {
					nextFireTime = getFireTimeAfter(nextFireTime);
					continue;
				}
			}
		}
	}

	@Override
	public boolean validateMisfireInstruction(int misfireInstruction) {
		if (misfireInstruction < MISFIRE_INSTRUCTION_SMART_POLICY) {
			return false;
		}

		if (misfireInstruction > GoDomain.MISFIRE_INSTRUCTION_DO_NOTHING) {
			return false;
		}

		return true;
	}

	/**
	 * <p>
	 * Determines whether the date and (optionally) time of the given Calendar 
	 * instance falls on a scheduled fire-time of this trigger.
	 * </p>
	 * 
	 * <p>
	 * Equivalent to calling <code>willFireOn(cal, false)</code>.
	 * </p>
	 * 
	 * @param test the date to compare
	 * 
	 * @see #willFireOn(Calendar, boolean)
	 */
	public boolean willFireOn(java.util.Calendar test) {
		return willFireOn(test, false);
	}

	/**
	 * <p>
	 * Determines whether the date and (optionally) time of the given Calendar 
	 * instance falls on a scheduled fire-time of this trigger.
	 * </p>
	 * 
	 * <p>
	 * Note that the value returned is NOT validated against the related
	 * org.quartz.Calendar (if any)
	 * </p>
	 * 
	 * @param test the date to compare
	 * @param dayOnly if set to true, the method will only determine if the
	 * trigger will fire during the day represented by the given Calendar
	 * (hours, minutes and seconds will be ignored).
	 * @see #willFireOn(Calendar)
	 */
	public boolean willFireOn(java.util.Calendar test, boolean dayOnly) {

		test = (java.util.Calendar) test.clone();

		test.set(java.util.Calendar.MILLISECOND, 0); // don't compare millis.

		if (dayOnly) {
			test.set(java.util.Calendar.HOUR_OF_DAY, 0);
			test.set(java.util.Calendar.MINUTE, 0);
			test.set(java.util.Calendar.SECOND, 0);
		}

		Date testTime = test.getTime();

		Date fta = getFireTimeAfter(new Date(test.getTime().getTime() - 1000));

		if (fta == null)
			return false;

		java.util.Calendar p = java.util.Calendar.getInstance(test.getTimeZone());
		p.setTime(fta);

		int year = p.get(java.util.Calendar.YEAR);
		int month = p.get(java.util.Calendar.MONTH);
		int day = p.get(java.util.Calendar.DATE);

		if (dayOnly) {
			return (year == test.get(java.util.Calendar.YEAR) && month == test.get(Calendar.MONTH) && day == test.get(java.util.Calendar.DATE));
		}

		while (fta.before(testTime)) {
			fta = getFireTimeAfter(fta);
		}

		if (fta.equals(testTime)) {
			return true;
		}

		return false;
	}
}
